/****************************************************************************
**
** WX公众号:Qt历险记
** CSDN:Qt魔术师
**
****************************************************************************/

#include <QtWidgets>

#include "mainwindow.h"

// 没有命令行参数文件名
MainWindow::MainWindow()
{
    qDebug()<<__LINE__<<"===================MainWindow()======================="<<Qt::endl;
    init();
    setCurrentFile(QString());
}

// 有命令行参数文件名
MainWindow::MainWindow(const QString &fileName)
{
    qDebug()<<__LINE__<<"===================MainWindow(const QString &fileName)======================="<<Qt::endl;
    init();
    loadFile(fileName);
}

// maybeSave() 是一个函数，用来检查是否需要保存数据。
// 如果需要保存，就调用 writeSettings() 来保存设置，然后通过 event->accept() 来接受关闭窗口的请求。
// 如果不需要保存，就通过 event->ignore() 来忽略关闭窗口的请求，让窗口继续显示。
void MainWindow::closeEvent(QCloseEvent *event)
{
    qDebug()<<__LINE__<<"===================closeEvent======================="<<Qt::endl;
    if (maybeSave()) {
        writeSettings(); // 存储属性坐标
        event->accept(); // 关闭窗口
    } else {
        event->ignore(); // 继续显示窗口
    }
}

// 新建文件
void MainWindow::newFile()
{
    MainWindow *other = new MainWindow;
    other->tile(this);
    other->show();
}

// 打开文件
void MainWindow::open()
{
    const QString fileName = QFileDialog::getOpenFileName(this);
    if (!fileName.isEmpty())
        openFile(fileName);
}

// 打开文件
void MainWindow::openFile(const QString &fileName)
{
    MainWindow *existing = findMainWindow(fileName);
    if (existing) {
        existing->show();
        existing->raise();          // 将它置于窗口堆叠的最前面（raise()
        existing->activateWindow(); // 顶级小部件设置为活动窗口
        return;
    }

    // 加载文件
    if (isUntitled && textEdit->document()->isEmpty() && !isWindowModified()) {
        loadFile(fileName);
        return;
    }

    MainWindow *other = new MainWindow(fileName);
    // is标题存在的
    if (other->isUntitled) {
        delete other;
        return;
    }
    other->tile(this);
    other->show();
}

// 保存文件
bool MainWindow::save()
{
    return isUntitled ? saveAs() : saveFile(curFile);
}

// 另存文件
bool MainWindow::saveAs()
{
    QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"),
                                                    curFile);
    if (fileName.isEmpty())
        return false;

    return saveFile(fileName);
}

// 关于Qt
void MainWindow::about()
{
   // 内嵌HTML标签语句
   QMessageBox::about(this, tr("关于sdi"),
            tr("<b>sdi</b>示例演示了如何使用qt编写单个文档接口应用程序。"));
}

// 当文档被修改时(输入内容嘛)，执行以下操作——将窗口标记为已修改 窗口标题将有一个“*”（星号）
void MainWindow::documentWasModified()
{
    qDebug()<<__LINE__<<"===================documentWasModified======================="<<Qt::endl;
    this->setWindowModified(true);
}

void MainWindow::init()
{
    qDebug()<<__LINE__<<"===================init======================="<<Qt::endl;
    // 这个setAttribute告诉程序，当你关闭窗口时，立即释放并清理窗口所占用的所有内存和资源
    setAttribute(Qt::WA_DeleteOnClose);

    isUntitled = true;

    textEdit = new QTextEdit;
    setCentralWidget(textEdit);

    // 创建右键菜单选项
    createActions();
    // 创建状态栏
    createStatusBar();

    // 读取注册表
    readSettings();

    connect(textEdit->document(), &QTextDocument::contentsChanged,
            this, &MainWindow::documentWasModified);

    setUnifiedTitleAndToolBarOnMac(true);
}

// 新建窗口 并且和之前的窗口位置不同
void MainWindow::tile(const QMainWindow *previous)
{
    qDebug()<<__LINE__<<"===================tile======================="<<Qt::endl;
    if (!previous)
        return;
    // 计算 previous 窗口的顶部边框宽度
    int topFrameWidth = previous->geometry().top() - previous->pos().y();
    if (!topFrameWidth)
        topFrameWidth = 40;
    // 使用前一个窗口的位置信息和计算得到的顶部边框宽度创建一个新的位置点 pos。
    const QPoint pos = previous->pos() + 2 * QPoint(topFrameWidth, topFrameWidth);
    // 检查屏幕的有效区域是否包含当前窗口的右下角位置和之前计算的 pos 位置，如果包含，则将当前窗口移动到 pos 指定的位置
    if (screen()->availableGeometry().contains(this->rect().bottomRight() + pos))
        this->move(pos);
}

//! [创建右键菜单选项]
void MainWindow::createActions()
{
    // 菜单的名称被设置为"&File"，其中的"&"符号表示快捷键。在这个例子中，如果用户按下"Alt+F"的组合键，就可以快速访问这个菜单。
    QMenu *fileMenu = menuBar()->addMenu(tr("文件"));
//! [工具栏]
    QToolBar *fileToolBar = addToolBar(tr("文件"));

    // 新建文件
    // fromTheme()函数根据主题名称获取相应的图标，QIcon(":/images/cut.png")表示如果主题中没有找到对应的图标，就使用位于":/images/cut.png"路径下的图标。
    const QIcon newIcon = QIcon::fromTheme("document-new", QIcon(":/images/new.png"));
    QAction *newAct = new QAction(newIcon, tr("新建"), this); // &支持快捷键
    newAct->setShortcuts(QKeySequence::New);       // 表示使用"Ctrl+N"组合键进行剪切操作
    newAct->setStatusTip(tr("创建新文件")); // 鼠标悬停在该操作上时显示的提示信息
    connect(newAct, &QAction::triggered, this, &MainWindow::newFile);
    fileMenu->addAction(newAct);
    fileToolBar->addAction(newAct);

    // 打开文件
    const QIcon openIcon = QIcon::fromTheme("document-open", QIcon(":/images/open.png"));
    QAction *openAct = new QAction(openIcon, tr("打开..."), this);
    openAct->setShortcuts(QKeySequence::Open);
    openAct->setStatusTip(tr("打开现有文件"));
    connect(openAct, &QAction::triggered, this, &MainWindow::open);
    fileMenu->addAction(openAct);
    fileToolBar->addAction(openAct);

    // 保存文件
    const QIcon saveIcon = QIcon::fromTheme("document-save", QIcon(":/images/save.png"));
    QAction *saveAct = new QAction(saveIcon, tr("保存"), this);
    saveAct->setShortcuts(QKeySequence::Save);
    saveAct->setStatusTip(tr("将文档保存到磁盘中"));
    connect(saveAct, &QAction::triggered, this, &MainWindow::save);
    fileMenu->addAction(saveAct);
    fileToolBar->addAction(saveAct);

    // 另存文件
    const QIcon saveAsIcon = QIcon::fromTheme("document-save-as");
    QAction *saveAsAct = fileMenu->addAction(saveAsIcon, tr("另存..."), this, &MainWindow::saveAs);
    saveAsAct->setShortcuts(QKeySequence::SaveAs);
    saveAsAct->setStatusTip(tr("以新名称保存文档"));

    // 添加分割线
    fileMenu->addSeparator();

    // 这个菜单项通常用于显示最近使用的文件列表 操作过的文件
    QMenu *recentMenu = fileMenu->addMenu(tr("最近操作文件..."));
    connect(recentMenu, &QMenu::aboutToShow, this, &MainWindow::updateRecentFileActions);
    recentFileSubMenuAct = recentMenu->menuAction();

    for (int i = 0; i < MaxRecentFiles; ++i) {
        recentFileActs[i] = recentMenu->addAction(QString(), this, &MainWindow::openRecentFile);
        recentFileActs[i]->setVisible(false);
    }

    recentFileSeparator = fileMenu->addSeparator();

    setRecentFilesVisible(MainWindow::hasRecentFiles());

    // 关闭
    QAction *closeAct = fileMenu->addAction(tr("关闭"), this, &QWidget::close);
    closeAct->setShortcut(tr("Ctrl+W")); // 手动指定快捷键
    closeAct->setStatusTip(tr("关闭此窗口"));

    // 应用程序退出
    const QIcon exitIcon = QIcon::fromTheme("application-exit");
    QAction *exitAct = fileMenu->addAction(exitIcon, tr("退出"), qApp, &QApplication::closeAllWindows); // 关闭所有窗口
    exitAct->setShortcuts(QKeySequence::Quit);
    exitAct->setStatusTip(tr("退出应用程序"));

    QMenu *editMenu = menuBar()->addMenu(tr("编辑"));
    QToolBar *editToolBar = addToolBar(tr("编辑"));

// 当你在编译Qt应用程序时定义了QT_NO_CLIPBOARD宏，它会告诉Qt编译器不包含剪贴板功能的支持。这意味着你的应用程序将无法使用Qt提供的剪贴板相关的类和方法。
#ifndef QT_NO_CLIPBOARD
    // 剪切
    const QIcon cutIcon = QIcon::fromTheme("edit-cut", QIcon(":/images/cut.png"));
    QAction *cutAct = new QAction(cutIcon, tr("剪切"), this);
    cutAct->setShortcuts(QKeySequence::Cut);
    cutAct->setStatusTip(tr("将当前选择的内容剪切到剪贴板中"));
    connect(cutAct, &QAction::triggered, textEdit, &QTextEdit::cut);
    editMenu->addAction(cutAct);
    editToolBar->addAction(cutAct);

    // 复制
    const QIcon copyIcon = QIcon::fromTheme("edit-copy", QIcon(":/images/copy.png"));
    QAction *copyAct = new QAction(copyIcon, tr("复制"), this);
    copyAct->setShortcuts(QKeySequence::Copy);
    copyAct->setStatusTip(tr("将当前选择的内容复制到剪贴板中"));
    connect(copyAct, &QAction::triggered, textEdit, &QTextEdit::copy);
    editMenu->addAction(copyAct);
    editToolBar->addAction(copyAct);

    // 粘贴
    const QIcon pasteIcon = QIcon::fromTheme("edit-paste", QIcon(":/images/paste.png"));
    QAction *pasteAct = new QAction(pasteIcon, tr("粘贴"), this);
    pasteAct->setShortcuts(QKeySequence::Paste);
    pasteAct->setStatusTip(tr("将剪贴板的内容粘贴到当前选择中"));
    connect(pasteAct, &QAction::triggered, textEdit, &QTextEdit::paste);
    editMenu->addAction(pasteAct);
    editToolBar->addAction(pasteAct);

    menuBar()->addSeparator();
#endif // !QT_NO_CLIPBOARD

    QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); // 快捷键Alt + H(h)
    QAction *aboutAct = helpMenu->addAction(tr("&About"), this, &MainWindow::about);
    aboutAct->setStatusTip(tr("显示应用程序的关于的方框"));

    QAction *aboutQtAct = helpMenu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
    aboutQtAct->setStatusTip(tr("显示Qt库的关于方框"));

// 当你在编译Qt应用程序时定义了QT_NO_CLIPBOARD宏，它会告诉Qt编译器不包含剪贴板功能的支持。这意味着你的应用程序将无法使用Qt提供的剪贴板相关的类和方法。
#ifndef QT_NO_CLIPBOARD // 没有 剪贴板
    cutAct->setEnabled(false);
    copyAct->setEnabled(false);
    // 文本框有内容时可剪切或者拷贝
    connect(textEdit, &QTextEdit::copyAvailable, cutAct, &QAction::setEnabled);
    connect(textEdit, &QTextEdit::copyAvailable, copyAct, &QAction::setEnabled);
#endif // !QT_NO_CLIPBOARD
}

// 状态栏信息
void MainWindow::createStatusBar()
{
    statusBar()->showMessage(tr("Ready"));
}

// 这段代码的作用是从应用程序的设置中读取窗口的位置和大小，并在下次打开程序时将窗口还原到之前的状态。
void MainWindow::readSettings()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    //  在settings里找到了窗口的位置和大小信息，如果找不到，就留空。
    const QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
    // 电脑的尺寸x y width height(0,0 1920x1040)
    qDebug()<<__LINE__<<" readSettings screen()->availableGeometry() = "<<screen()->availableGeometry()<<Qt::endl;

    // 如果没有找到位置和大小信息，窗口会根据可用屏幕大小的三分之一和二分之一进行调整，并移到屏幕正中央；
    // 如果找到了信息，就将窗口位置和大小还原为之前保存的状态。
    if (geometry.isEmpty()) {
        const QRect availableGeometry = screen()->availableGeometry();
        // 窗口大小 三个窗口=我的电脑宽度  两个窗口=我的电脑高度
        this->resize(availableGeometry.width() / 3, availableGeometry.height() / 2);
        // 窗口坐标 居中
        this->move((availableGeometry.width() - width()) / 2,
             (availableGeometry.height() - height()) / 2);
    } else {
        // 恢复上一次窗口的位置
        this->restoreGeometry(geometry);
    }
}

// 写注册表信息
void MainWindow::writeSettings()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    settings.setValue("geometry", this->saveGeometry());
}

// 修改过文档 关闭时提示用户是否保存文件提示
bool MainWindow::maybeSave()
{
    if (!textEdit->document()->isModified())
        return true;
    const QMessageBox::StandardButton ret
        = QMessageBox::warning(this, tr("SDI"),
                               tr("该文档已被修改。\n"
                                  "您是否要保存您所做的更改?"),
                               QMessageBox::Save | QMessageBox::Discard
                               | QMessageBox::Cancel);
    switch (ret) {
    case QMessageBox::Save:
        return save();
    case QMessageBox::Cancel:
        return false;
    default:
        break;
    }
    return true;
}

// 加载文件(读取文件)
void MainWindow::loadFile(const QString &fileName)
{

    QFile file(fileName);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("SDI"),
                             tr("无法读取文件 %1:\n%2.")
                             .arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return;
    }

    QTextStream in(&file);
    // 这里的主要目的是在读取文本内容期间显示等待光标，以提醒用户程序在处理中。
    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
    textEdit->setPlainText(in.readAll());
    QGuiApplication::restoreOverrideCursor();

    setCurrentFile(fileName);
    statusBar()->showMessage(tr("文件加载"), 5000);
}

// 设置最新操作的文件可见
void MainWindow::setRecentFilesVisible(bool visible)
{
    recentFileSubMenuAct->setVisible(visible);
    recentFileSeparator->setVisible(visible);
}

static inline QString recentFilesKey() { return QStringLiteral("recentFileList"); }
static inline QString fileKey() { return QStringLiteral("file"); }

// 读取最近的文件
static QStringList readRecentFiles(QSettings &settings)
{
    QStringList result;
    // 这行代码通过settings对象开始读取最近文件列表的数组，并返回数组的大小。
    const int count = settings.beginReadArray(recentFilesKey()); // recentFileList
    for (int i = 0; i < count; ++i) {
        // 这行代码将settings对象设置为当前索引i指定的数组元素。
        settings.setArrayIndex(i);
        // 键->file | 值->文件的绝对路径名 这一行将当前索引指定的文件名添加到result列表中。
        result.append(settings.value(fileKey()).toString());
        qDebug()<<"readRecentFiles fileKey() = "<<fileKey()<<" settings.value(fileKey()).toString() = "<<settings.value(fileKey()).toString();
    }
    settings.endArray();
    // 函数返回存储最近文件列表的字符串列表result
    return result;
}

// 写入最近的文件 这是一个静态函数，接受一个字符串列表files和一个QSettings对象settings作为参数。
static void writeRecentFiles(const QStringList &files, QSettings &settings)
{
    const int count = files.size();
    // 这一行告诉settings对象开始写入最近文件列表的数组。
    settings.beginWriteArray(recentFilesKey());
    for (int i = 0; i < count; ++i) {
        // 这行代码将settings对象设置为当前索引i指定的数组元素。
        settings.setArrayIndex(i);
        // 这一行将文件列表中的文件名写入到settings对象的数组中。
        settings.setValue(fileKey(), files.at(i));
    }
    settings.endArray();
}

// 有当前操作过的文件 就返回true
bool MainWindow::hasRecentFiles()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    // 开始读取名为recentFilesKey()的最近文件列表数组。这个函数返回该数组的大小（即文件数量）。
    // settings.endArray()结束对最近文件列表数组的读取。
    const int count = settings.beginReadArray(recentFilesKey());
    settings.endArray();

    // hasRecentFiles organizationName =  "QtProject"  applicationName =  "SDI Example"
    qDebug()<<"hasRecentFiles organizationName = "<<QCoreApplication::organizationName()
            <<" applicationName = "<<QCoreApplication::applicationName()<<" recentFilesKey = "<<recentFilesKey()
            <<" count = "<<count;
    return count > 0;
}

// 准备最近的文件
void MainWindow::prependToRecentFiles(const QString &fileName)
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());

    const QStringList oldRecentFiles = readRecentFiles(settings);
    QStringList recentFiles = oldRecentFiles;
    recentFiles.removeAll(fileName); // 删除列表中所有同名fileName
    /*
        Example:
          QList<QString> list;
          list.prepend("one");
          list.prepend("two");
          list.prepend("three");
          // list: ["three", "two", "one"]
    */
    recentFiles.prepend(fileName);   // 在列表的开头插入值 放在第一个
    qDebug()<<"oldRecentFiles = "<<oldRecentFiles<<" recentFiles = "<<recentFiles;

    if (oldRecentFiles != recentFiles)
        writeRecentFiles(recentFiles, settings);

    setRecentFilesVisible(!recentFiles.isEmpty());
}

void MainWindow::updateRecentFileActions()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());

    const QStringList recentFiles = readRecentFiles(settings);
    const int count = qMin(int(MaxRecentFiles), recentFiles.size());
    int i = 0;
    for ( ; i < count; ++i) {
        const QString fileName = MainWindow::strippedName(recentFiles.at(i));
        recentFileActs[i]->setText(tr("&%1 %2").arg(i + 1).arg(fileName));
        recentFileActs[i]->setData(recentFiles.at(i));
        recentFileActs[i]->setVisible(true);
    }
    for ( ; i < MaxRecentFiles; ++i)
        recentFileActs[i]->setVisible(false);
}

void MainWindow::openRecentFile()
{
    if (const QAction *action = qobject_cast<const QAction *>(sender()))
        openFile(action->data().toString());
}

// 保存文件
bool MainWindow::saveFile(const QString &fileName)
{
    QString errorMessage;

    // 告诉应用程序要覆盖默认的鼠标指针，将其设置为一个等待鼠标指针，以提示用户正在等待某个操作完成【如果文件很大需要这样操作】
    QGuiApplication::setOverrideCursor(Qt::WaitCursor);

    // QSaveFile提供了一个安全地写入文件的接口
    QSaveFile file(fileName);
    if (file.open(QFile::WriteOnly | QFile::Text)) {
        QTextStream out(&file);
        out << textEdit->toPlainText(); // 写入文件
        // 如果写入操作成功，就提交保存
        if (!file.commit()) {
            errorMessage = tr("无法写入文件 %1:\n%2.")
                           .arg(QDir::toNativeSeparators(fileName), file.errorString());
        }
    } else {
        errorMessage = tr("无法打开文件 %1 用于写作:\n%2.")
                //  在windows上，toNativeSeparators("c:/winnt/system32") 返回 "c:\winnt\system32".
                       .arg(QDir::toNativeSeparators(fileName), file.errorString());
    }
    // 恢复鼠标光标
    QGuiApplication::restoreOverrideCursor();

    if (!errorMessage.isEmpty()) {
        QMessageBox::warning(this, tr("SDI"), errorMessage);
        return false;
    }

    // 保存后操作的还是当前文件 所以窗口标题显示当前操作的文件名
    setCurrentFile(fileName);
    statusBar()->showMessage(tr(fileName.toUtf8()+" 保存成功。"), 5000); // 5s后消失
    return true;
}

// 设置当前文件名在窗口
void MainWindow::setCurrentFile(const QString &fileName)
{
    static int sequenceNumber = 1;

    isUntitled = fileName.isEmpty();
    if (isUntitled) {
        curFile = tr("document%1.txt").arg(sequenceNumber++);
    } else {
        curFile = QFileInfo(fileName).canonicalFilePath(); // 返回文件的绝对路径
    }

    textEdit->document()->setModified(false);
    this->setWindowModified(false); // 窗口标题将有一个“*”（星号）

    qDebug()<<"setCurrentFile windowFilePath() = "<<windowFilePath()<<" curFile = "<<curFile;
    if (!isUntitled && windowFilePath() != curFile)
        MainWindow::prependToRecentFiles(curFile);

    // 设置窗口文件路径
    this->setWindowFilePath(curFile);
}

// 返回文件名(QtPro.txt) 例如：C://Users//Desktop//QtPro.txt
QString MainWindow::strippedName(const QString &fullFileName)
{
    return QFileInfo(fullFileName).fileName();
}


// 代码的作用是在应用程序的所有顶层窗口中查找具有指定文件路径的主窗口，并返回该主窗口的指针
MainWindow *MainWindow::findMainWindow(const QString &fileName) const
{
    // 返回文件的标准绝对路径 C:/Users/Desktop/QtPro/sdi/document1.txt
    QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
    qDebug()<<"【findMainWindow】-》canonicalFilePath = "<<canonicalFilePath;

    // 获取应用程序中所有顶层窗口的列表
    const QList<QWidget *> topLevelWidgets = QApplication::topLevelWidgets();
    // 窗口的当前文件路径等于指定的规范化文件路径，则返回该窗口指针
    for (QWidget *widget : topLevelWidgets) {
        MainWindow *mainWin = qobject_cast<MainWindow *>(widget);
        if (mainWin && mainWin->curFile == canonicalFilePath)
            return mainWin;
    }

    return nullptr;
}
